# grnn_de_recommender.py
import numpy as np
from sklearn.preprocessing import StandardScaler
from scipy.spatial.distance import cdist
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import logging
from sqlalchemy import text

logging.basicConfig(level=logging.DEBUG)


class GRNN_DE_Recommender:
    def __init__(self,user_id=None):
        self.user_id = user_id  # 存储用户 ID
        self.scaler = StandardScaler()
        self.best_sigma = None
        self.best_centers = None
        self.user_features, self.item_features = {}, {}
        self.user_features_array, self.item_features_array = None, None
        self.user_ids, self.item_ids = [], []
        self.ratings = {}

    # ------------------------------------------------- #
    # 数据加载                                           #
    # ------------------------------------------------- #
    def load_data(self, session):
        # ----------① 用户 ----------
        q_user = """
        SELECT u.id,
               uf.english_level,
               uf.target_score,
               AVG(r.correctness) AS accuracy,
               AVG(r.time_spent)  AS avg_time,
               GROUP_CONCAT(DISTINCT i.type) AS weak_types
        FROM tb_user u
        JOIN tb_user_feature uf ON u.id = uf.uid
        JOIN tb_rate r          ON u.id = r.uid
        JOIN tb_item i          ON r.iid = i.id
        GROUP BY u.id
        """
        for uid, level, score, acc, avg_t, weak in session.execute(text(q_user)):
            self.user_ids.append(uid)
            self.user_features[uid] = {
                "level": level,
                "score": score,
                "acc": acc,
                "avg_t": avg_t,
                "weak": weak or "",
            }

        # ----------② 试题 ----------
        q_item = """
        SELECT id, type, difficulty, score_avg,
               vocabulary_level, sentence_complexity
        FROM tb_item
        """
        for (
                iid,
                itype,
                diff,
                s_avg,
                v_lvl,
                sent_cplx,
        ) in session.execute(text(q_item)):
            self.item_ids.append(iid)
            self.item_features[iid] = {
                "type": itype,
                "diff": diff,
                "s_avg": s_avg,
                "v_lvl": v_lvl,
                "s_cplx": sent_cplx,
            }

        # ----------③ 评分 ----------
        for uid, iid, rate in session.execute(text("SELECT uid, iid, rate FROM tb_rate")):
            self.ratings.setdefault(uid, {})[iid] = rate

        logging.debug(
            "数据加载完成：用户%d，试题%d，评分%d",
            len(self.user_ids),
            len(self.item_ids),
            sum(len(v) for v in self.ratings.values()),
        )
        # 调试信息改用 self.user_id
        if self.user_id:
            logging.debug("当前登录用户ID: %s", self.user_id)
            logging.debug("用户ID是否在列表中: %s", self.user_id in self.user_ids)

    # ------------------------------------------------- #
    # 特征预处理（统一 8 维）                            #
    # ------------------------------------------------- #
    @staticmethod
    def _type_flags(typename: str) -> list[int]:
        flags = [0, 0, 0, 0]  # 完型/阅读/翻译/写作
        if "完型" in typename:
            flags[0] = 1
        elif "阅读" in typename:
            flags[1] = 1
        elif "翻译" in typename:
            flags[2] = 1
        elif "写作" in typename:
            flags[3] = 1
        return flags

    def preprocess_features(self):
        lvl_map = {"四级": 1, "六级": 2, "专八": 3}
        user_mat = []
        for uid in self.user_ids:
            f = self.user_features[uid]
            user_mat.append(
                [
                    lvl_map.get(f["level"], 0),
                    float(f["score"]) / 100,
                    float(f["acc"]),
                    float(f["avg_t"]) / 300,
                    *self._type_flags(f["weak"]),
                    ]
            )

        item_mat = []
        for iid in self.item_ids:
            f = self.item_features[iid]
            item_mat.append(
                [
                    float(f["diff"]),
                    float(f["s_avg"]),
                    float(f["v_lvl"]) / 10,
                    float(f["s_cplx"]),
                    *self._type_flags(f["type"]),
                    ]
            )

        # 同一 scaler，维度都是 (n, 8)
        self.user_features_array = self.scaler.fit_transform(np.array(user_mat))
        self.item_features_array = self.scaler.transform(np.array(item_mat))

    # ------------------------------------------------- #
    # GRNN + DE                                         #
    # ------------------------------------------------- #
    @staticmethod
    def grnn_predict(X, centers, sigma):
        d = cdist(X, centers)
        hidden = np.exp(-(d**2) / (2 * sigma**2))
        return hidden.sum(axis=1) / hidden.shape[1]

    def _fitness(self, params, X1, y1, X2, y2):
        sigma = params[0]
        centers = params[1:].reshape(-1, X1.shape[1])
        p1 = self.grnn_predict(X1, centers, sigma)
        p2 = self.grnn_predict(X2, centers, sigma)
        return 0.3 * mean_squared_error(y1, p1) + 0.7 * mean_squared_error(y2, p2)

    def _de(self, X1, y1, X2, y2, pop=30, gen=80, F=0.8, CR=0.9):
        nf, nc = X1.shape[1], 10
        bounds = [(0.1, 2.0)] + [
            (X1[:, i].min(), X1[:, i].max()) for _ in range(nc) for i in range(nf)
        ]
        P = np.random.uniform(
            low=[b[0] for b in bounds], high=[b[1] for b in bounds], size=(pop, len(bounds))
        )
        for _ in range(gen):
            newP = []
            for j in range(pop):
                a, b, c = P[np.random.choice([i for i in range(pop) if i != j], 3, False)]
                mutant = np.clip(a + F * (b - c), [b[0] for b in bounds], [b[1] for b in bounds])
                trial = np.where(np.random.rand(len(bounds)) < CR, mutant, P[j])
                newP.append(trial if self._fitness(trial, X1, y1, X2, y2)
                                     < self._fitness(P[j], X1, y1, X2, y2) else P[j])
            P = np.array(newP)
        best = P[np.argmin([self._fitness(p, X1, y1, X2, y2) for p in P])]
        self.best_sigma = best[0]
        self.best_centers = best[1:].reshape(nc, nf)

    # ------------------------------------------------- #
    # 训练                                              #
    # ------------------------------------------------- #
    def train(self, session):
        self.load_data(session)
        if not self.user_ids or not self.item_ids or not self.ratings:
            raise ValueError("数据不足，无法训练")

        self.preprocess_features()

        X, y = [], []
        uid2idx = {u: i for i, u in enumerate(self.user_ids)}
        iid2idx = {i: j for j, i in enumerate(self.item_ids)}
        for uid, items in self.ratings.items():
            if uid not in uid2idx:
                continue
            u_vec = self.user_features_array[uid2idx[uid]]
            for iid, rate in items.items():
                if iid not in iid2idx:
                    continue
                i_vec = self.item_features_array[iid2idx[iid]]
                X.append(np.abs(u_vec - i_vec))
                y.append(rate)

        X, y = np.array(X), np.array(y)
        X1, X2, y1, y2 = train_test_split(X, y, test_size=0.2, random_state=42)
        self._de(X1, y1, X2, y2)
        val_mse = mean_squared_error(
            y2, self.grnn_predict(X2, self.best_centers, self.best_sigma)
        )
        val_rmse = np.sqrt(val_mse)  # 手动计算平方根得到RMSE
        logging.info("模型训练完成，验证集 RMSE=%.4f", val_rmse)

    # ------------------------------------------------- #
    # 推荐                                              #
    # ------------------------------------------------- #
    def recommend(self, user_id, top_k=10):
        if user_id not in self.user_ids:
            raise ValueError(f"用户 {user_id} 不存在")
        u_vec = self.user_features_array[self.user_ids.index(user_id)]
        scores = []
        for iid, i_idx in zip(self.item_ids, range(len(self.item_ids))):
            diff = np.abs(u_vec - self.item_features_array[i_idx]).reshape(1, -1)
            s = self.grnn_predict(diff, self.best_centers, self.best_sigma)[0]
            scores.append((iid, float(s)))
        scores.sort(key=lambda x: x[1], reverse=True)
        return scores[:top_k]


# ---------------------- 单元测试 ---------------------- #
if __name__ == "__main__":
    from flask import session
    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker

    def get_current_user_id():
        # 检查是否存在用户ID
        user_id = session.get('user_id')
        if not user_id:
            raise ValueError("用户未登录")
        return user_id

    try:
        current_user_id = get_current_user_id()
        print(f"当前用户 ID: {current_user_id}")

        recommender = GRNN_DE_Recommender(user_id=current_user_id)

        # 创建SQLAlchemy session
        engine = create_engine('mysql+pymysql://root:1234@localhost:3306/flask_job')
        Session = sessionmaker(bind=engine)
        db_session = Session()

        try:
            # 训练模型
            recommender.train(db_session)

            # 为当前用户推荐
            recommendations = recommender.recommend(current_user_id)
            print(f"→ 为您推荐的题目: {recommendations}")

        finally:
            db_session.close()

    except Exception as e:
        logging.error("运行出错: %s", str(e))